// 활용 사례
Slack 에서 자연어로 어제 매출 묻기
슬랙에 "어제 매출?" 한 줄을 던지면 봇이 입금 합계로 답한다. headless 거래내역 API 와 슬랙 봇을 60줄로 잇는 방법.
월요일 아침마다 누군가 은행 사이트에 들어가 주말 입금을 더하고, 그 숫자를 슬랙에 옮겨 적는다. 그 일을 봇에 맡기면 채널에 어제 매출? 한 줄만 남는다.
이 글은 슬랙 봇 하나를 60줄로 만들어 headless 거래내역 API 에 잇는다. 셋업까지 약 15분, 그다음부터는 질문 한 줄.
어떤 상황을 풀고 있나
- 매출을 매일 확인하는 사람: 대표, 재무 담당, 이커머스 운영자
- 빈도: 매일~매주. 정산일이 몰리면 하루에도 여러 번
- 지금 방식: 은행 사이트 로그인 → 거래내역 다운로드 → 엑셀에서 입금만 필터 → 슬랙에 복사
마지막 단계만 자동화하는 것이 아니라 처음부터 끝까지 봇에게 맡긴다.
결과 — 한 화면
슬랙 채널에서:
@매출봇 어제 매출?
# 잠시 후 …
2026-05-26 입금 18건. 합계 ₩7,420,000.
가장 큰 입금은 ㈜에이클라이언트 ₩2,200,000.@매출봇 지난주 매출? 처럼 기간을 바꿔 물어도 같은 흐름으로 답한다.
1. 준비물
| 준비물 | 역할 | 비용 |
|---|---|---|
| headless API key | 거래내역 수집 | 무료로 시작 |
| 은행 자격증명 1건 | 입금이 찍히는 계좌 | — |
| Slack 앱 (봇 토큰) | 메시지 송수신 | 무료 |
| Node 18+ 런타임 | 봇 프로세스 | 로컬·서버 무관 |
자격증명은 한 번만 등록한다. headless 가 봉투 암호화로 보관하고, 수집 시점에만 메모리에서 복호화한다.
curl -X POST https://api.h6s.ai/api/v1/credentials \
-H "Authorization: Bearer $H6S_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"providerCode": "CB_KB",
"authMethod": "ID_PW",
"credentials": { "loginId": "...", "loginPw": "..." }
}'
# → { "id": "cred_...", "credentialHealth": "UNKNOWN" }2. 핵심 코드 — 60줄 이내
슬랙 Bolt 앱 하나가 멘션을 받아 headless 에 거래내역을 요청하고, 입금만 더해 답한다.
import pkg from "@slack/bolt";
const { App } = pkg;
const API = "https://api.h6s.ai/api/v1";
const headers = {
Authorization: `Bearer ${process.env.H6S_API_KEY}`,
"Content-Type": "application/json",
};
// data-job 1건을 만들고 끝날 때까지 폴링한 뒤 결과를 돌려준다
async function fetchTransactions(start, end) {
const job = await fetch(`${API}/data-jobs`, {
method: "POST",
headers,
body: JSON.stringify({
credentialId: process.env.H6S_CREDENTIAL_ID,
schema: "bank.transactions.cb.v1",
dateRangeStart: start,
dateRangeEnd: end,
}),
}).then((r) => r.json());
let status = job.status;
// 예제는 30초 가드만 — 프로덕션은 타임아웃·재시도·에러 핸들링을 더 둔다
for (let i = 0; (status === "PENDING" || status === "RUNNING") && i < 15; i++) {
await new Promise((r) => setTimeout(r, 2000));
const s = await fetch(`${API}/data-jobs/${job.id}`, { headers }).then((r) => r.json());
status = s.status;
}
if (status !== "SUCCESS") throw new Error(`job ${job.id} 미완료: ${status}`);
return fetch(`${API}/data-jobs/${job.id}/results`, { headers }).then((r) => r.json());
}
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
app.event("app_mention", async ({ event, say }) => {
const day = /지난주/.test(event.text) ? 7 : 1;
const end = new Date();
const start = new Date(Date.now() - day * 86400000);
const iso = (d) => d.toISOString().slice(0, 10);
const { records } = await fetchTransactions(iso(start), iso(end));
const deposits = records.filter((r) => r.amount > 0);
const total = deposits.reduce((s, r) => s + r.amount, 0);
const top = deposits.sort((a, b) => b.amount - a.amount)[0];
await say(
`${iso(start)}~${iso(end)} 입금 ${deposits.length}건. 합계 ₩${total.toLocaleString()}.\n` +
(top ? `가장 큰 입금은 ${top.description} ₩${top.amount.toLocaleString()}.` : "입금 없음."),
);
});
await app.start(3000);bank.transactions.cb.v1 의 amount 는 입금이 양수, 출금이 음수다. 어느 은행에서 받아도 8개 필드가 같다. 위 필터를 그대로 쓴다.
3. 배포
봇은 멘션을 기다리는 상주 프로세스다. cron 이 아니라 늘 떠 있어야 한다.
- 가장 단순한 길: 작은 서버나 컨테이너에
node bot.js상주 - secret 4개를 환경변수로:
H6S_API_KEY,H6S_CREDENTIAL_ID,SLACK_BOT_TOKEN,SLACK_SIGNING_SECRET - 첫 확인: 채널에 봇을 초대하고
@매출봇 어제 매출?한 줄
기본 Bolt 구성에서는 이벤트를 받자마자 200 을 응답하고 핸들러를 비동기로 돌리므로 폴링이 길어도 슬랙이 재시도하지 않지만, ack 를 핸들러 완료 뒤로 미루는 환경(예: AWS Lambda 리시버, processBeforeResponse: true)에서는 이벤트를 먼저 ack 하고 결과를 client.chat.postMessage 로 따로 보내는 편이 안전하다.
4. 운영 — 한 달 뒤 무엇을 보는가
- 호출량: 하루 한두 번 물으면 월 50건 안쪽. 무료 플랜의 월 수집 요청 한도(20건)를 넘기면 스탠다드로 올린다
- 자격증명 만료: 응답이 비거나
failureCategory: CREDENTIAL이 보이면 콘솔에서 자격증명을 갱신한다 - 타임존:
dateRangeStart/End는 날짜 단위다. "어제" 의 경계가 KST 기준인지 한 번 확인한다
5. 다른 데 붙이기
같은 골격에서 데이터 형식과 집계만 바꾸면 다른 자동화가 된다.
- 세금계산서 합계 →
hometax.tax-invoices.sales.v1 - 매출 추세를 시트에 누적 → 데이터베이스/스프레드시트로 append
- 정기 리포트 → 멘션 대신 cron 으로 매일 아침 자동 전송
더 깊이
같은 골격을 다른 채널로 옮기려면 표준 MCP 클라이언트 연동부터 보면 된다. MCP 연동 문서에 설치와 인증이 정리돼 있다.
봇이 한 번 답하기 시작하면, 다음은 묻지 않아도 매일 아침 먼저 알려주는 쪽이다. 멘션 핸들러를 cron 으로 바꾸면 그대로 된다.